package edu.amm.neural;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Распознаватель образов, построенный на двухслойной сети встречного распространения.
 * Скрытый слой сети представляет собой сеть нейронов Кохонена.
 * Выходной слой сети связывается со скрытым посредством звёзд Гроссберга.
 *
 * Распознаватель работает в предположении, что между множеством кластеров входных образов и множеством классов,
 * к которым их относят, есть взаимно однозначное соответствие.
 *
 * На вход распознавателя поступает образ в виде вещественного вектора.
 * На выходе распознаватель выдаёт номер класса, к которому он относит соответствующий образ.
 *
 * @author Иванов Илья
 * @since 2014.03.03
 */
public class KohonenGrossbergRecognizer {

	/** Начальные веса инициализируются значениями из отрезка [- INIT_WEIGHT_BOUND, INIT_WEIGHT_BOUND]. */
	private static final double INIT_WEIGHT_BOUND = 1E-3;

	/** Ширина отрезка для генерации начальных весов. */
	private static final double INIT_SEGMENT_WIDTH = 2 * INIT_WEIGHT_BOUND;

	/** Начальный темп обучения. */
	private static final double INIT_TRAINING_TEMP = 0.99;

	/** Темп уменьшения темпа обучения. */
	private static final double DECR_TRAINING_TEMP = 0.9;

	/** Начальное значение максимального расстояния между весовыми векторами. */
	private static final double INIT_MAX_DIST = 3;

	/** Темп уменьшения максимального расстояния между весовыми векторами. */
	private static final double DECR_MAX_DIST = 0.8;

	/** Максимальное количество полных обходов обучающего множества при обучении сети. */
	private static final int MAX_TRAINING_STAGES = 10;

	/** Величина суммарного изменения весов, при которой процесс обучения останавливается. */
	private static final double MAX_VAR = 1E-3;

	private int patternDim;
	private int classesCount;

	/** Матрица весов скрытого слоя размерности classesCount x patternDim. Хранится в транспонированном по отношнею к
	 * традиционной форме виде: строки соответствуют нейронам, столбцы - входам сети. По сути является массивом
	 * эталонных векторов соответствующих нейронам кластеров (массив центров кластеров).
	 */
	private double[][] wt;

	/**
	 * Отображение нейронов кохонена в номера классов: индекс элемента массива - номер нейрона кохонена, значение
	 * элемента по индексу - номер соответствующего класса.
	 */
	private int[] kohClassMap;

	private double trainingTemp;
	private double maxDist;

	/**
	 * Создаёт распознаватель с заданными параметрами.
	 *
	 * @param patternDim Размерность пространства образов.
	 * @param classesCount Количество классов образов, на которые обучается распознаватель.
	 */
	public KohonenGrossbergRecognizer(int patternDim, int classesCount) {
		if (patternDim < 1 || classesCount < 1) {
			throw new RuntimeException("Некорректные параметры распознавателя");
		}

		this.patternDim = patternDim;
		this.classesCount = classesCount;

		wt = new double[classesCount][patternDim];
		kohClassMap = new int[classesCount];

		for (int i = 0; i < classesCount; i++) {
			kohClassMap[i] = -1;
		}
	}

	/**
	 * Инициализирует процесс обучения сети.
	 */
	public void reset() {
		initialize();
	}

	/**
	 * Классифицирует образ. Сам образ в процессе работы метода будет модифицирован!
	 *
	 * @param pattern Образ. Будет модифицирован в процессе работы метода!
	 * @return Номер класса в диапазоне от 0 до (classesCount - 1) включительно, к которому распознаватель отнёс образ,
	 * или -1, если распознаватель не смог классифицировать образ.
	 */
	public int classify(double[] pattern) {
		if (pattern == null || pattern.length != patternDim) {
			throw new RuntimeException("Некорректный входной образ");
		}

		normalize(pattern);
		int kohInd = findNearestNeuron(pattern);

		return kohClassMap[kohInd];
	}

	/**
	 * Запускает процесс обучения на заданном множестве пар "образ - класс".
	 * В процессе обучения образы обучающего множества будут модифицированы!
	 *
	 * @param pairs Пары обучающего множества. В процессе обучения образы обучающего множества будут модифицированы!
	 */
	public void train(List<PatternClassPair> pairs) {
		checkPairs(pairs);

		// Нормируем образы из обучающего множества.
		for (PatternClassPair pair : pairs) {
			normalize(pair.getPattern());
		}

		// Матрица для хранения весов с предыдущего полного обхода обучающего множества.
		double[][] prevWt = new double[classesCount][patternDim];
		// Суммарное абсолютное изменение весов при очередном полном обходе обучающего множества.
		double sumVar = Double.MAX_VALUE;
		// Количество совершённых полных обходов обучающего множества.
		int stageCount = 0;

		while (stageCount < MAX_TRAINING_STAGES && sumVar > MAX_VAR) {
			copyMatr(wt, prevWt);

			for (PatternClassPair pair : pairs) {
				trainOnPair(pair);
			}

			sumVar = distance(wt, prevWt);
			stageCount++;
		}

		// Подсчитваем в рамках каждого кластера количества пар, отнесённых учителем к каждому классу.
		// Ключ - номер кластера (номер нейрона), значение - отображение: ключ - номер класса (номер выхода), значение -
		// количество пар, отнесённых учителем к этому классу.
		Map<Integer, Map<Integer, Integer>> numbers = new HashMap<Integer, Map<Integer, Integer>>();
		for (PatternClassPair pair : pairs) {
			Integer leaderIndex = findNearestNeuron(pair.getPattern());

			Map<Integer, Integer> map = numbers.get(leaderIndex);
			if (map == null) {
				numbers.put(leaderIndex, map = new HashMap<Integer, Integer>());
			}

			Integer count = map.get(pair.getClassNumber());
			if (count == null) {
				map.put(pair.getClassNumber(), 1);
			} else {
				map.put(pair.getClassNumber(), count + 1);
			}
		}

		// Финальное сопоставление нейронов Кохонена с выходами сети.
		// Каждому кластеру ставится в соответствие тот номер класса, к которому учителем было отнесено наибольшее
		// количество пар, находящихся в данном кластере.
		for (Map.Entry<Integer, Map<Integer, Integer>> entry : numbers.entrySet()) {
			int classNumber = -1;
			int maxCount = Integer.MIN_VALUE;
			for (Map.Entry<Integer, Integer> innerEntry : entry.getValue().entrySet()) {
				int currentClassNumber = innerEntry.getKey();
				int currentCount = innerEntry.getValue();
				if (currentCount > maxCount) {
					classNumber = currentClassNumber;
					maxCount = currentCount;
				}
			}

			int neuronIndex = entry.getKey();
			kohClassMap[neuronIndex] = classNumber;
		}
	}

	/**
	 * Обучает сеть на одной паре.
	 *
	 * @param pair Обучающая пара.
	 */
	public void train(PatternClassPair pair) {
		checkPair(pair);
		normalize(pair.getPattern());
		trainOnPair(pair);
	}

	/**
	 * Обучает сеть на одной паре.
	 *
	 * @param pair Обучающая пара.
	 */
	private void trainOnPair(PatternClassPair pair) {
		double[] x = pair.getPattern();

		int leaderInd = findNearestNeuron(x);
		double[] leader = wt[leaderInd];

		// Настройка весов нейрона leader и всех нейронов, находящихся от него на расстоянии,
		// не превышающем maxDist.
		for (int j = 0; j < classesCount; j++) {
			if (j == leaderInd || distance(leader, wt[j]) <= maxDist) {
				for (int i = 0; i < patternDim; i++) {
					wt[j][i] += trainingTemp * (x[i] - wt[j][i]);
				}

				normalize(wt[j]);
			}
		}

		// Соотносим индекс победившего нейрона с номером класса.
		int classNumber = pair.getClassNumber();
		kohClassMap[leaderInd] = classNumber;

		trainingTemp *= DECR_TRAINING_TEMP;
		maxDist *= DECR_MAX_DIST;
	}

	/**
	 * Нормирует вектор по евклидовой норме. Результирующий нормированный вектор перезаписывает исходный вектор.
	 *
	 * @param x Вектор для нормировки.
	 */
	private static void normalize(double[] x) {
		double sum = 0;
		for (double xi : x) {
			sum += xi * xi;
		}

		if (sum == 0) {
			return;
		}

		sum = Math.sqrt(sum);

		for (int i = 0; i < x.length; i++) {
			x[i] /= sum;
		}
	}

	/**
	 * Находит для данного входного вектора ближайший нейрон. Критерий близости заключается в минимуме расстояния
	 * от вектора до центра кластера, представленного нейроном.
	 *
	 * @param x Входной вектор, для которого ищется ближайший нейрон.
	 * @return Индекс ближайшего нейрона: число от 0 до (classesCount - 1).
	 */
	private int findNearestNeuron(double[] x) {
		double min = Double.MAX_VALUE;
		int minJ = 0;
		for (int j = 0; j < classesCount; j++) {
			double dist = distance(x, wt[j]);
			if (dist < min) {
				min = dist;
				minJ = j;
			}
		}

		return minJ;
	}

	/**
	 * Вычисляет евклидово расстояние между двумя векторами.
	 *
	 * @param x Первый вектор.
	 * @param y Второй вектор.
	 * @return Евклидово расстояние между векторами.
	 */
	private static double distance(double[] x, double[] y) {
		double sum = 0;
		for (int i = 0; i < x.length; i++) {
			double diff = x[i] - y[i];
			sum += diff * diff;
		}

		sum = Math.sqrt(sum);
		return sum;
	}

	/**
	 * Проверяет корректность пар обучающего множества.
	 *
	 * @param pairs Множество пар для проверки.
	 */
	private void checkPairs(List<PatternClassPair> pairs) {
		if (pairs == null || pairs.isEmpty()) {
			throw new RuntimeException("Не задано обучающее множество пар");
		}

		for (PatternClassPair pair : pairs) {
			checkPair(pair);
		}
	}

	/**
	 * Проверят корректность обучающей пары. В случае некорректной пары генерирует RuntimeException.
	 *
	 * @param pair Пара для проверки
	 */
	private void checkPair(PatternClassPair pair) {
		if (pair == null || pair.getPattern() == null || pair.getPattern().length != patternDim ||
		    pair.getClassNumber() < 0 || pair.getClassNumber() >= classesCount) {
			throw new RuntimeException("Некорректное обучающее множество пар");
		}
	}

	/**
	 * Инициализация сети: весовым коэффициентам присваиваются малые случайные значения.
	 */
	private void initialize() {
		Random rnd = new Random();

		for (int j = 0; j < classesCount; j++) {
			for (int i = 0; i < patternDim; i++) {
				wt[j][i] = -INIT_WEIGHT_BOUND + INIT_SEGMENT_WIDTH * rnd.nextDouble();
			}

			normalize(wt[j]);
		}

		trainingTemp = INIT_TRAINING_TEMP;
		maxDist = INIT_MAX_DIST;
	}

	/**
	 * Копирует одну матриу в другую.
	 *
	 * @param src Исходная матрица.
	 * @param dest Целевая матрица.
	 */
	private void copyMatr(double[][] src, double[][] dest) {
		int rows = src.length;

		for (int i = 0; i < rows; i++) {
			int columns = src[i].length;

			for (int j = 0; j < columns; j++) {
				dest[i][j] = src[i][j];
			}
		}
	}

	/**
	 * Находит расстояние между двумя матрицами в смысле нормы, вычисляемой как сумма модулей всех элементов матрицы.
	 *
	 * @param matrX Первая матрица.
	 * @param matrY Вторая матрица.
	 * @return Расстояние между матрицами.
	 */
	private double distance(double[][] matrX, double[][] matrY) {
		double dist = 0;
		int rows = matrX.length;

		for (int i = 0; i < rows; i++) {
			int columns = matrX[i].length;

			for (int j = 0; j < columns; j++) {
				dist += Math.abs(matrX[i][j] - matrY[i][j]);
			}
		}

		return dist;
	}
}
